在完成使用者註冊、題庫建置,以及程度測驗的功能後,將要進一步優化作答體驗,加入提示系統。這個功能的核心概念是,當使用者遇到困難時,可以點選「提示」,獲得一些解題方向或思路,但同時為了避免濫用,若使用提示,系統會在計分時進行扣分處理。
在現有 Problem 模型加入 hint,以儲存每題的提示。
# backend/models.py
from sqlalchemy import Column, Integer, String, Boolean, ForeignKey, DateTime, Text
from sqlalchemy.orm import relationship
from datetime import datetime
from .database import Base
class Problem(Base):
__tablename__ = "problems"
id = Column(Integer, primary_key=True, index=True)
slug = Column(String, unique=True, index=True)
title = Column(String, index=True)
difficulty = Column(String)
topic = Column(String)
is_active = Column(Boolean, default=True)
hint = Column(String, nullable=True) # 新增:提示文字,可為 None
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
email = Column(String, unique=True, index=True)
name = Column(String)
level = Column(String, default="beginner")
is_active = Column(Boolean, default=True)
submissions = relationship("Submission", back_populates="user")
class Submission(Base):
__tablename__ = "submissions"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"))
problem_id = Column(Integer, ForeignKey("problems.id"))
language = Column(String, default="python")
code = Column(Text)
verdict = Column(String, default="pending") # pending / accepted / wrong / tle ...
created_at = Column(DateTime, default=datetime.utcnow)
user = relationship("User", back_populates="submissions")
讓題目輸入輸出(ProblemCreate、ProblemOut)與抽題輸出(AssessmentProblem)都包含 hint。
在原本的 backend/dto.py 基礎上,為下列類別加入 hint 欄位(其它類別不變)。
# backend/dto.py
from pydantic import BaseModel, ConfigDict
from typing import Optional, Literal, List
class ProblemOut(BaseModel):
id: int
slug: str
title: str
difficulty: str
topic: str
hint: Optional[str] = None # 回傳也帶 hint
model_config = ConfigDict(from_attributes=True)
class ProblemCreate(BaseModel):
slug: str
title: str
difficulty: Literal["Easy", "Medium", "Hard"]
topic: str
hint: Optional[str] = None # 建立時可帶 hint
class AssessmentProblem(BaseModel):
id: int
slug: str
title: str
difficulty: str
topic: str
hint: Optional[str] = None # 抽題回應帶 hint
model_config = ConfigDict(from_attributes=True)
讓新增題目時可以一併傳 hint
from fastapi import APIRouter, Depends, Query, HTTPException, status
from sqlalchemy.orm import Session
from typing import List, Optional
from ..database import SessionLocal
from .. import models
from ..dto import ProblemOut, ProblemCreate
router = APIRouter(prefix="/problems", tags=["problems"])
def get_db():
db = SessionLocal()
try: yield db
finally: db.close()
@router.get("", response_model=List[ProblemOut])
def list_problems(difficulty: Optional[str] = Query(None),
topic: Optional[str] = Query(None),
q: Optional[str] = Query(None),
db: Session = Depends(get_db)):
qs = db.query(models.Problem).filter(models.Problem.is_active == True)
if difficulty: qs = qs.filter(models.Problem.difficulty == difficulty)
if topic: qs = qs.filter(models.Problem.topic == topic)
if q: qs = qs.filter(models.Problem.title.ilike(f"%{q}%"))
return qs.order_by(models.Problem.id.asc()).all()
@router.post("", response_model=ProblemOut, status_code=status.HTTP_201_CREATED)
def create_problem(payload: ProblemCreate, db: Session = Depends(get_db)):
# 確保 slug 唯一
exists = db.query(models.Problem).filter(models.Problem.slug == payload.slug).first()
if exists: raise HTTPException(409, "slug already exists")
# payload.model_dump() 會包含 hint(可為 None)
obj = models.Problem(**payload.model_dump(), is_active=True)
db.add(obj); db.commit(); db.refresh(obj)
return obj
讓種子資料直接帶提示,方便前端測試按鈕與扣分。
MOCK = [
{"slug":"two-sum","title":"Two Sum","difficulty":"Easy","topic":"Array",
"hint":"用哈希表 O(n) 找 complement"},
{"slug":"longest-substring","title":"Longest Substring Without Repeating Characters",
"difficulty":"Medium","topic":"HashMap",
"hint":"滑動視窗 + 記錄最後出現位置"},
{"slug":"median-of-two-sorted-arrays","title":"Median of Two Sorted Arrays",
"difficulty":"Hard","topic":"Binary Search",
"hint":"對短陣列做二分,維持左右分割平衡"}
]
在 Day 3 的測驗流程上,加入「顯示提示」按鈕。按了之後:
import streamlit as st, requests, os
API_BASE = os.getenv("API_BASE", "http://127.0.0.1:8000")
st.header("程度測驗(3 題快速分級)- 含提示")
# 狀態初始化:抽題結果、各題是否看過提示、各題回報
if "assessment_user_id" not in st.session_state:
st.session_state.assessment_user_id = 1
if "assessment_problems" not in st.session_state:
st.session_state.assessment_problems = None
if "assessment_results" not in st.session_state:
st.session_state.assessment_results = {}
if "hint_shown" not in st.session_state:
st.session_state.hint_shown = {} # {problem_id: bool}
# 抽題
if st.button("開始測驗 / 重新抽題"):
try:
resp = requests.get(f"{API_BASE}/assessment/start",
params={"user_id": st.session_state.assessment_user_id}, timeout=8)
problems = resp.json()["problems"]
st.session_state.assessment_problems = problems
st.session_state.assessment_results = {}
st.session_state.hint_shown = {p["id"]: False for p in problems}
st.success("已抽出題目,請回報結果或查看提示")
except Exception as e:
st.error(f"抽題失敗:{e}")
# 顯示題目 + 提示按鈕 + 回報結果
if st.session_state.assessment_problems:
st.write("請到 LeetCode 作答,或根據理解回報本題結果:")
for p in st.session_state.assessment_problems:
pid = p["id"]
st.markdown(f"- **[{p['difficulty']}] {p['title']}** / topic: {p['topic']} / slug: `{p['slug']}`")
# 有 hint 才顯示按鈕;按下就顯示並記錄使用提示
if p.get("hint"):
if not st.session_state.hint_shown.get(pid, False):
if st.button(f"顯示提示(題目 #{pid})", key=f"hintbtn_{pid}"):
st.session_state.hint_shown[pid] = True
st.info(p["hint"])
else:
st.info(p["hint"])
verdict = st.selectbox(
f"結果(題目 #{pid})", ["accepted", "wrong", "tle", "skipped"], key=f"verdict_{pid}"
)
hint_used = st.session_state.hint_shown.get(pid, False) # ← 關鍵:是否按過提示
st.session_state.assessment_results[pid] = {
"problem_id": pid, "verdict": verdict, "hint_used": hint_used
}
# 送出評分
if st.button("送出並計分"):
try:
payload = {
"user_id": st.session_state.assessment_user_id,
"items": list(st.session_state.assessment_results.values())
}
resp = requests.post(f"{API_BASE}/assessment/score", json=payload, timeout=10)
data = resp.json()
if resp.status_code >= 400:
st.error(f"計分失敗:{data}")
else:
st.success(f"建議等級:**{data['level']}**(score={data['score']})")
with st.expander("查看每題計分"):
for pid, info in data["breakdown"].items():
st.write(f"- #{pid} {info['title']} / {info['difficulty']} → "
f"{info['verdict']} / hint={info['hint_used']} / score={info['score']}")
except Exception as e:
st.error(f"計分失敗:{e}")
做完以上步驟後,執行
後端
python -m uvicorn backend.app:app --reload
前端
cd frontend
python -m streamlit run app.py
按「開始測驗 / 重新抽題」→ 有 hint 的題會看到按鈕。
按下提示 → 畫面顯示提示內容;送分後 breakdown 中該題 hint_used: true,分數-0.2。
今天已經成功完成提示系統的基本功能,包含前端顯示提示按鈕、後端儲存與回傳提示文字,以及評分時自動扣分的邏輯。使用者在作答過程中,如果遇到瓶頸,可以選擇點擊提示按鈕,系統就會顯示預先設定的思路,協助他們找到解題方向。此功能能幫助卡關的使用者順利推進,但同時若使用提示,也會在分數計算上有相應的扣分,避免濫用功能,未來計畫加入自動提示生成,讓系統能根據題目類型自動產出思路,進一步提升互動性與智慧化程度。